package com.leyou.search.service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.page.pageResult;
import com.leyou.common.utils.BeanHelper;
import com.leyou.common.utils.JsonUtils;
import com.leyou.item.dto.*;
import com.leyou.itme.client.ItemClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.GoodsDTO;
import com.leyou.search.pojo.SearchRequestParam;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.data.querydsl.QPageRequest;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
public class SearchService {


    @Autowired
    private ItemClient itemClient;

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    /*构建Goods对象:
    * 1.Goods对象为创建搜索索引库的文档
    * 任务目录：将商品数据从数据库 导入 索引库
    * 数据库中的商品数据 spu
    * 所以要想构建 文档数据，需要参数 数据库的商品数据 spu
    *
    * 2.商品微服务对外提供的商品数据对象为spuDTO，参数为 spuDTO
    *
    * */
    public Goods createGoods(SpuDTO spuDTO){

        /*1.用于拼接all 搜索的字段信息*/
        StringBuilder stringBuilder=new StringBuilder();
        /*1.获取分类一二三级的名称，现有的数据一二三级分类的id：可通过id查询分类的名称：调用远程服务
        * 参数：分类的id集合ids
        * */
        List<CategoryDTO> categoryDTOList = itemClient.findCategoryByIds(spuDTO.getCategoryIds());
       /*获取名称：采用stream*/
        String categoryName = categoryDTOList.stream().map(categoryDTO -> categoryDTO.getName()).collect(Collectors.joining(","));
        /*查询品牌名称：根据品牌id：最终获取品牌名称*/
        BrandDTO brandDTO = itemClient.findBrandById(spuDTO.getBrandId());
        String allString = stringBuilder.append(spuDTO.getName()).append(",").append(categoryName).append(",").append(brandDTO.getName()).toString();


        /*2.价格数据：获取spu下所有的sku，遍历，获取价格，采用Set集合存储
        * 根据spu_id查询所有的sku数据
        * */

        List<SkuDTO> skuDTOList = itemClient.findSkuBySpuId(spuDTO.getId());
        Set<Long> priceSet = skuDTOList.stream().map(SkuDTO::getPrice).collect(Collectors.toSet());


        /*3.sku集合采用json存储;[{}，{}....]
        * {"id":2, "title":"xx", "price":299900, "image":""},
        * 直接采用json构造数据
        * */
        JSONArray jsonArray=new JSONArray();
        skuDTOList.stream().forEach(skuDTO -> {
            JSONObject json = new JSONObject();
            json.put("id", skuDTO.getId());
            json.put("title", skuDTO.getTitle());
            json.put("price", skuDTO.getPrice());
            json.put("image", skuDTO.getImages());
            jsonArray.add(json);
        });

        /*规格参数*/
        SpuDetailDTO spuDetailDTO = itemClient.findSpuDetailBySupId(spuDTO.getId());
        /*获取通用参数*/
        String genericSpec = spuDetailDTO.getGenericSpec();
        /*将通用属性字符串转为map：采用工具类JsonUtils.toMap(字符串,键的类型,值的类型);
        *
        * 在JsonUtils工具类中，采用ObjectMapper mapper = new ObjectMapper();实例转换json数据
        * 1.如果每一次转换都要创建该实例，性能相比fastjson较低
        *  public static final ObjectMapper mapper =new ObjectMapper();
        * 2.当前工具类采用静态ObjectMapper实例，一次创建，可多次使用，性能比fastjson要高
        * 3.利用fastjson也能完成转换；
        * */
        Map<Long, Object> genericSpecMap = JsonUtils.toMap(genericSpec, Long.class, Object.class);

        /*获取特殊属性*/
        String specialSpec = spuDetailDTO.getSpecialSpec();

        /*将特殊属性字符串转为map*/
        Map<Long, List> specialSpecMap = JsonUtils.toMap(specialSpec, Long.class, List.class);

        /*获取规格参数的名称：采用分类id查询，用第三级分类cid3*/
        List<SpecParamDTO> specParamDTOList = itemClient.findSpecParam(null, spuDTO.getCid3(), true);

        /*最终将规格参数存储到map：
        * 1.创建map容器
        * 2.遍历规格参数集合，判断是否为通用属性.再获取参数值
        * 3.获取参数名称和值，放入map存储
        * */
        Map<String,Object> map=new HashMap<>();

        specParamDTOList.stream().forEach(specParamDTO -> {

             String name = specParamDTO.getName();

             Long id = specParamDTO.getId();

            /*必须定义一个Object的value：因为当出现数值类型的数据时，
            需要先获取当前的参数值，再进行转化区间*/
            Object value=null;
            /*判断当前的参数是否为通用属性，
             *   方法二：
             *  判断map集合中是否存在这个键
             *   map.containsKey()
             *   相比利用数据字段直接判断效率低些
             *   方法一：利用该实体的字段值，判断，效率高些
             * */
            if (specParamDTO.getGeneric()){
               /*获取参数值*/
                value=genericSpecMap.get(id);
            }else {
                value=specialSpecMap.get(id);
            }
            if(specParamDTO.getIsNumeric()){
                value=chooseSegment(value,specParamDTO);
            }
             map.put(name,value);

        });

        Goods goods = new Goods();
        goods.setId(spuDTO.getId());//spu_id
        goods.setSubTitle(spuDTO.getSubTitle());//卖点 sub_title(副标题)
        goods.setAll(allString);//用于搜索的组合字段：一二三级分类的名称CategoryName，名牌名称brandName，商品名称sup_name
        goods.setBrandId(spuDTO.getBrandId());//品牌id
        goods.setCategoryId(spuDTO.getCid3());//三级分类id
        goods.setCreateTime(spuDTO.getCreateTime());//创建时间
        goods.setPrice(priceSet);//价格 采用set集合存储
        goods.setSkus(jsonArray.toJSONString());//sku集合，采用json字符串存储：包含的数据：image图片，title标题 id
        goods.setSpecs(map);//规格参数

        return goods;
    }


    private String chooseSegment(Object value, SpecParamDTO p) {
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }
        double val = parseDouble(value.toString());
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    private double parseDouble(String str) {
        try {
            return Double.parseDouble(str);
        } catch (Exception e) {
            return 0;
        }
    }

    public pageResult<GoodsDTO> searchGoodsListByPage(SearchRequestParam searchRequestParam) {

        String key=searchRequestParam.getKey();
        if (StringUtils.isBlank(key)) {
            throw new LyException(ExceptionEnum.INVALID_PARAM_ERROR);
        }
        int page=searchRequestParam.getPage();
        int size=searchRequestParam.getSize();

        /*参数值：searchRquestParam:封装了 查询的关键字 key
                 分页的参数 page  显示的记录数默认为20，采用默认值
        * */
       /*
       * POST goods/_search
            {
              "_source":["id","subTitle","skus"],
              "query": {
                "match": {
                  "all": "手机"
                }
              }
              ,
              "from": 0,
              "size": 20
            }
       *基础搜索：采用match
       * 匹配查询(match)会把查询条件进行分词，然后进行查询，多个词条之间默认关系是OR，要改为and
       *
       *1.创建原生查询构造器
       * 2.指定查询显示的字段
       * 3.构造查询方式和字段，match改为and
       * 4.分页条件的构建
       * 5.采用ElasticSearchTemplate发送请求查询
       *
       *
       *
       * */

        NativeSearchQueryBuilder queryBuilder=new NativeSearchQueryBuilder();

        /*指定显示的字段:
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[] {},null))

        public NativeSearchQueryBuilder withSourceFilter(参数SourceFilter sourceFilter)
        参数1：SourceFilter类型的接口，采用实现类FetchSourceFilter（）传递
        public class FetchSourceFilter implements SourceFilter
        参数2：FetchSourceFilter的参数为 String[]，数组中指定显示的字段，
        参数3：excludes:null
       public FetchSourceFilter(String[] includes, String[] excludes)

        */

        /*构造查询方式和字段*/queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","subTitle","skus"},null));

        queryBuilder.withQuery(basicSearchQuery(searchRequestParam));

        /*分页构造：采用PageRequest.of(page-1,size):ES中分页，开始page默认从0开始*/
        //不推荐使用 Pageable pageable=new QPageRequest(page-1,size);
        /*QPageRequest(int page, int size)
         不推荐使用。 从2.1开始，of(int, int)改用。*/
        queryBuilder.withPageable(PageRequest.of(page-1,size));

        /*执行查询ElasticSearchTemplate:
        elasticsearchTemplate.queryForPage(queryBuilder.build(), Goods.class);
        * 参数1：queryBuilder.build();创建实例
        * 参数2：查询的索引Goods中指定的索引库
        * */
        AggregatedPage<Goods> forPage = elasticsearchTemplate.queryForPage(queryBuilder.build(), Goods.class);

        /*封装查询的结果*/
        if(forPage==null|| CollectionUtils.isEmpty(forPage.getContent())){
            throw  new LyException(ExceptionEnum.GOODS_NOT_FOUND);
        }
        /*对象转换*/
        List<GoodsDTO> goodsDTOList = BeanHelper.copyWithCollection(forPage.getContent(), GoodsDTO.class);

        /*封装分页数据*/
        pageResult pageResult=new pageResult();
        pageResult.setTotal(forPage.getTotalElements());
        pageResult.setTotalPage(Long.valueOf(forPage.getTotalPages()));
        pageResult.setItems(goodsDTOList);

        return pageResult;

    }

    public Map<String, List<?>> queryFilters(SearchRequestParam searchRequestParam) {
        /*参数：searchRequestParam: 包含搜索的关键字 key 和 分页的数据page:
        * 此次是进行  搜索条件过滤，所以不考虑分页等功能，
        * 利用Elasticsearch的聚合功能，进行搜索条件的聚合
        * 1.商品分类的 2.品牌信息
        *   搜索过滤的条件
        *
        *
        *   POST goods/_search
                {
                  "_source":["id","subTitle","skus"],
                  "query": {
                    "match": {
                      "all": "手机"
                    }
                  }
                  ,
                  "from": 0,
                  "size": 2,
                  "aggs": {
                    "brandId": {
                      "terms": {
                        "field": "brandId"
                      }
                    },
                    "categoryId":{
                      "terms": {
                        "field": "categoryId"
                      }
                    }
                  }
                }
        *
        * */
        /*过滤条件存储在map*/
         Map<String,List<?>> map=new HashMap<>();
         //搜索关键字
         String key=searchRequestParam.getKey();
         if(StringUtils.isBlank(key)) {
            throw new LyException(ExceptionEnum.INVALID_PARAM_ERROR);
         }
         /*原生查询构造器*/
         NativeSearchQueryBuilder queryBuilder=new NativeSearchQueryBuilder();
         /*指定的显示列，此次不用显示*/
         queryBuilder.withSourceFilter(new FetchSourceFilterBuilder().build());
         /*搜索结果默认进行10条数据的显示，当指定此次查询分页的条件为1条，那反而提高了查询效率
         * 因为此次不需要关心分页，只对聚合结果进行分析
         * */
         queryBuilder.withQuery(basicSearchQuery(searchRequestParam));
         queryBuilder.withPageable(PageRequest.of(0,1));

         /*添加分类聚合*/
         String categoryAgg="categoryAgg";
         queryBuilder.addAggregation(AggregationBuilders.terms(categoryAgg).field("categoryId"));
         /*添加品牌聚合*/
         String brandAgg="brandAgg";
         queryBuilder.addAggregation(AggregationBuilders.terms(brandAgg).field("brandId"));
         /*执行搜索*/
        AggregatedPage<Goods> goodsAggregatedPage = elasticsearchTemplate.queryForPage(queryBuilder.build(), Goods.class);
        /*获取聚合的结果集*/
        Aggregations aggregations = goodsAggregatedPage.getAggregations();

        //一.获取分类聚合的结果
        /*获取聚合结果时，不要用最大的接口，要用结果相对应的类型的实现类接收
        * 对 分类 的id进行聚合，id为Long类型，采用LongTerms接收
        * */
           LongTerms categoryAggregation = aggregations.get(categoryAgg);
          /*对分类聚合的结果进行分析*/

           handleCategoryAgg(categoryAggregation,map,searchRequestParam);

          //获取品牌的聚合结果集
          /*获取品牌id集合*/
         /*调远程服务，查询品牌的集合，获取品牌信息：名称和图片*/
         LongTerms BrandAggregation = aggregations.get(brandAgg);
        /*对品牌聚合结果的分析*/
           handleBrandAgg(BrandAggregation,map);
        /*存入map*/
        return map;
    }

    private void  handleCategoryAgg(LongTerms categoryAggregation, Map<String, List<?>> map,SearchRequestParam searchRequestParam) {
        /*获取分类的id集合*/
        /*调用远程服务，查询分类集合，获取分类的名称*/
        List<LongTerms.Bucket> CategoryBucketList = categoryAggregation.getBuckets();
        List<Long> categoryIds = CategoryBucketList.stream().map(bucket -> {
            long value = bucket.getKeyAsNumber().longValue();
            return value;
        }).collect(Collectors.toList());
        if(categoryIds!=null&&categoryIds.size()==1){
               /*证明只有一种商品分类：可进行规格参数的过滤*/
               /*调用远程服务：查询规格参数*/
                handleSpecAgg(categoryIds.get(0),map,searchRequestParam);
        }
        List<CategoryDTO> categoryDTOList= itemClient.findCategoryByIds(categoryIds);
        if(CollectionUtils.isEmpty(categoryDTOList)){
            throw  new LyException(ExceptionEnum.CATEGORY_NOT_FOUND);
        }
        /*添加分类集合*/
        map.put("分类",categoryDTOList);

    }
    public  void handleBrandAgg(LongTerms BrandAggregation, Map<String, List<?>> map){

        List<LongTerms.Bucket> BrandBucketList = BrandAggregation.getBuckets();
        if (CollectionUtils.isEmpty(BrandBucketList)){
            throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
        }
        List<Long> brandIds = BrandBucketList.stream().map(bucket -> {
            long value = bucket.getKeyAsNumber().longValue();
            return value;
        }).collect(Collectors.toList());
        List<BrandDTO> brandDTOList = itemClient.findBrandByIds(brandIds);
        if (CollectionUtils.isEmpty(brandDTOList)){
            throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
        }
        map.put("品牌",brandDTOList);
    }


    public void handleSpecAgg(Long cid,Map<String, List<?>> map,SearchRequestParam searchRequestParam){
        /*过滤参数也应根据搜索的结果进行过滤：在搜索条件基础上，对搜索结果聚合`*/
        /*调用远程服务：查询规格参数*/
        List<SpecParamDTO> specParamDTOList = itemClient.findSpecParam(null, cid, true);
        if(CollectionUtils.isEmpty(specParamDTOList)){
            throw  new LyException(ExceptionEnum.SPEC_NOT_FOUND);
        }
        /*获取查询的关键字*/
        String key=searchRequestParam.getKey();
        /*构造原生查询器，重新查询数据*/
        NativeSearchQueryBuilder queryBuilder=new NativeSearchQueryBuilder();
        /*设置显示的列，为null即可*/
        queryBuilder.withSourceFilter(new FetchSourceFilterBuilder().build());
        /*分页设置*/
        queryBuilder.withPageable(PageRequest.of(0,1));

        /*设置查询的关键字*/
        queryBuilder.withQuery(basicSearchQuery(searchRequestParam));
        /*设置聚合:如何设置聚合参数的名称
        *
        * 因为规格参数保存时是specs的属性，因此所有的规格参数都会有一个`specs.`的前缀
        * 如：specs.CUP  specs.屏幕尺寸 等  即：specs.参数名称
        *因为：聚合的参数有多个，所以也是多个聚合，那么聚合的名称为 参数的名称即可，且不会重复
        * 方案：遍历数据库查询出的多个规格参数，设置每一个参数的聚合
        * */
        specParamDTOList.stream().forEach(specParamDTO -> {

            String name=specParamDTO.getName();//name:为每个参数聚合的名称，也为每一个聚合的参数进行指定
            /*设置聚合的参数值*/
            queryBuilder.addAggregation(AggregationBuilders.terms(name).field("specs."+name));

        });
        /*执行查询*/
        AggregatedPage<Goods> goodsAggregated = elasticsearchTemplate.queryForPage(queryBuilder.build(), Goods.class);
        /*分析结果*/
        Aggregations aggregations = goodsAggregated.getAggregations();
        /*获取聚合的参数，还需要聚合的名称，需要遍历pecParamDTOList获取*/
        specParamDTOList.stream().forEach(specParamDTO -> {

            String name=specParamDTO.getName();

            /*获取聚合的参数*/
            StringTerms terms=aggregations.get(name);

            /*获取桶*/
            List<StringTerms.Bucket> bucketList = terms.getBuckets();

            /*过滤掉为NULL的参数项*/
            List<String> stringList = bucketList.stream().map(StringTerms.Bucket::getKeyAsString).filter(StringUtils::isNotEmpty).collect(Collectors.toList());
            map.put(name,stringList);
        });
    }

    private QueryBuilder basicSearchQuery(SearchRequestParam searchRequestParam) {

        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        /*获取搜索的关键字*/
        String key = searchRequestParam.getKey();

        /*获取搜索过滤筛选的条件*/
        Map<String, String> filters = searchRequestParam.getFilter();

        if (!CollectionUtils.isEmpty(filters)) {

         /*   Set<Map.Entry<String, String>> entries = filters.entrySet();
            entries.stream().forEach(Entry -> {
                String entryKey = Entry.getKey();
                String entryValue = Entry.getValue();
            });*/
            /*遍历filter的map集合*/
            for (Map.Entry<String, String> entry : filters.entrySet()) {
                // 获取过滤条件的key
                String FilterKey = entry.getKey();
                // 规格参数的key要做前缀specs.
                if ("分类".equals(FilterKey)) {
                    FilterKey = "categoryId";
                } else if ("品牌".equals(FilterKey)) {
                    FilterKey = "brandId";
                } else {
                    FilterKey = "specs." + FilterKey;
                }
                // value
                String FilterValue = entry.getValue();
                /*指定组合搜索*/

                queryBuilder.
                        must(
                                QueryBuilders.matchQuery("all", key).operator(Operator.AND)
                        ).
                        filter(
                                QueryBuilders.termQuery(FilterKey, FilterValue)
                        );

            }

        }else {
            queryBuilder.
                    must(
                            QueryBuilders.matchQuery("all", key).operator(Operator.AND)
                    );
        }
        return queryBuilder;
    }
}
